/*
 * Copyright (C) 2013 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.picasso;

import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.net.NetHandle;
import ohos.net.NetManager;
import ohos.security.SystemPermission;

import java.util.*;
import java.util.concurrent.ExecutorService;

import static com.squareup.picasso.BitmapHunter.forRequest;
import static com.squareup.picasso.MemoryPolicy.shouldWriteToMemoryCache;
import static com.squareup.picasso.Utils.*;

class Dispatcher {
    private static final int RETRY_DELAY = 500;
    private static final int AIRPLANE_MODE_ON = 1;
    private static final int AIRPLANE_MODE_OFF = 0;

    static final int REQUEST_SUBMIT = 1;
    static final int REQUEST_CANCEL = 2;
    static final int REQUEST_GCED = 3;
    static final int HUNTER_COMPLETE = 4;
    static final int HUNTER_RETRY = 5;
    static final int HUNTER_DECODE_FAILED = 6;
    static final int HUNTER_DELAY_NEXT_BATCH = 7;
    static final int HUNTER_BATCH_COMPLETE = 8;
    static final int NETWORK_STATE_CHANGE = 9;
    static final int AIRPLANE_MODE_CHANGE = 10;
    static final int TAG_PAUSE = 11;
    static final int TAG_RESUME = 12;
    static final int REQUEST_BATCH_RESUME = 13;

    private static final String DISPATCHER_THREAD_NAME = "Dispatcher";
    private static final int BATCH_DELAY = 200; // ms

    final EventRunner dispatcherThread;
    final Context context;
    final ExecutorService service;
    final Downloader downloader;
    final Map<String, BitmapHunter> hunterMap;
    final Map<Object, Action> failedActions;
    final Map<Object, Action> pausedActions;
    final Set<Object> pausedTags;
    final EventHandler handler;
    final EventHandler mainThreadHandler;
    final Cache cache;
    final Stats stats;
    final List<BitmapHunter> batch;
    final boolean scansNetworkChanges;

    boolean airplaneMode;

    Dispatcher(Context context, ExecutorService service, EventHandler mainThreadHandler,
        Downloader downloader, Cache cache, Stats stats) {
        this.dispatcherThread = EventRunner.create(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME);
        Utils.flushStackLocalLeaks(dispatcherThread);
        this.context = context;
        this.service = service;
        this.hunterMap = new LinkedHashMap<String, BitmapHunter>();
        this.failedActions = new WeakHashMap<>();
        this.pausedActions = new WeakHashMap<>();
        this.pausedTags = new LinkedHashSet<>();
        this.handler = new DispatcherHandler(dispatcherThread, this);
        this.downloader = downloader;
        this.mainThreadHandler = mainThreadHandler;
        this.cache = cache;
        this.stats = stats;
        this.batch = new ArrayList<BitmapHunter>(4);
        this.airplaneMode = Utils.isAirplaneModeOn(this.context);
        this.scansNetworkChanges = hasPermission(context, SystemPermission.GET_NETWORK_INFO);
    }

    void shutdown() {
        // Shutdown the thread pool only if it is the one created by Picasso.
        if (service instanceof PicassoExecutorService) {
            service.shutdown();
        }
        downloader.shutdown();
        dispatcherThread.stop();
        // Unregister network broadcast receiver on the main thread.
        Picasso.HANDLER.postTask(new Runnable() {
            @Override
            public void run() {
            }
        });
    }

    void dispatchSubmit(Action action) {
        handler.sendEvent(InnerEvent.get(REQUEST_SUBMIT, action));
    }

    void dispatchCancel(Action action) {
        handler.sendEvent(InnerEvent.get(REQUEST_CANCEL, action));
    }

    void dispatchPauseTag(Object tag) {
        handler.sendEvent(InnerEvent.get(TAG_PAUSE, tag));
    }

    void dispatchResumeTag(Object tag) {
        handler.sendEvent(InnerEvent.get(TAG_RESUME, tag));
    }

    void dispatchComplete(BitmapHunter hunter) {
        handler.sendEvent(InnerEvent.get(HUNTER_COMPLETE, hunter));
    }

    void dispatchRetry(BitmapHunter hunter) {
        handler.sendEvent(InnerEvent.get(HUNTER_RETRY, hunter), RETRY_DELAY);
    }

    void dispatchFailed(BitmapHunter hunter) {
        handler.sendEvent(InnerEvent.get(HUNTER_DECODE_FAILED, hunter));
    }

    void dispatchAirplaneModeChange(boolean airplaneMode) {
        handler.sendEvent(InnerEvent.get(AIRPLANE_MODE_CHANGE, airplaneMode ? AIRPLANE_MODE_ON : AIRPLANE_MODE_OFF, null));
    }

    void performSubmit(Action action) {
        performSubmit(action, true);
    }

    void performSubmit(Action action, boolean dismissFailed) {
        if (pausedTags.contains(action.getTag())) {
            pausedActions.put(action.getTarget(), action);

            if (action.getPicasso().loggingEnabled) {
                log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
                    "because tag '" + action.getTag() + "' is paused");
            }
            return;
        }

        BitmapHunter hunter = hunterMap.get(action.getKey());
        if (hunter != null) {
            hunter.attach(action);
            return;
        }

        if (service.isShutdown()) {
            if (action.getPicasso().loggingEnabled) {
                log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
            }
            return;
        }

        hunter = forRequest(action.getPicasso(), this, cache, stats, action);
        hunter.future = service.submit(hunter);
        hunterMap.put(action.getKey(), hunter);
        if (dismissFailed) {
            failedActions.remove(action.getTarget());
        }

        if (action.getPicasso().loggingEnabled) {
            log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
        }
    }

    void performAirplaneModeChange(boolean airplaneMode) {
        this.airplaneMode = airplaneMode;
    }

    void performRetry(BitmapHunter hunter) {
        if (hunter.isCancelled()) return;

        if (service.isShutdown()) {
            performError(hunter, false);
            return;
        }

        NetManager netManager = NetManager.getInstance(context);

        NetHandle netHandle = netManager.getDefaultNet();
        boolean isConnect = false;

        boolean status = netManager.sendNetConnectState(netHandle, isConnect);

        if (hunter.shouldRetry(airplaneMode)) {
            if (hunter.getPicasso().loggingEnabled) {
                log(OWNER_DISPATCHER, VERB_RETRYING, getLogIdsForHunter(hunter));
            }
            if (hunter.getException() instanceof NetworkRequestHandler.ContentLengthException) {
                hunter.networkPolicy |= NetworkPolicy.NO_CACHE.index;
            }
            hunter.future = service.submit(hunter);
        } else {
            // Mark for replay only if we observe network info changes and support replay.
            boolean willReplay = scansNetworkChanges && hunter.supportsReplay();
            performError(hunter, willReplay);
            if (willReplay) {
                markForReplay(hunter);
            }
        }
    }

    void performCancel(Action action) {
        String key = action.getKey();
        BitmapHunter hunter = hunterMap.get(key);
        if (hunter != null) {
            hunter.detach(action);
            if (hunter.isCancelled()) {
                hunterMap.remove(key);
                if (action.getPicasso().loggingEnabled) {
                    log(OWNER_DISPATCHER, VERB_CANCELED, action.getRequest().logId());
                }
            }
        }

        if (pausedTags.contains(action.getTag())) {
            pausedActions.remove(action.getTarget());
            if (action.getPicasso().loggingEnabled) {
                log(OWNER_DISPATCHER, VERB_CANCELED, action.getRequest().logId(),
                    "because paused request got canceled");
            }
        }

        Action remove = failedActions.remove(action.getTarget());
        if (remove != null && remove.getPicasso().loggingEnabled) {
            log(OWNER_DISPATCHER, VERB_CANCELED, remove.getRequest().logId(), "from replaying");
        }
    }

    void performPauseTag(Object tag) {
        // Trying to pause a tag that is already paused.
        if (!pausedTags.add(tag)) {
            return;
        }

        // Go through all active hunters and detach/pause the requests
        // that have the paused tag.
        for (Iterator<BitmapHunter> it = hunterMap.values().iterator(); it.hasNext();) {
            BitmapHunter hunter = it.next();
            boolean loggingEnabled = hunter.getPicasso().loggingEnabled;

            Action single = hunter.getAction();
            List<Action> joined = hunter.getActions();
            boolean hasMultiple = joined != null && !joined.isEmpty();

            // Hunter has no requests, bail early.
            if (single == null && !hasMultiple) {
                continue;
            }

            if (single != null && single.getTag().equals(tag)) {
                hunter.detach(single);
                pausedActions.put(single.getTarget(), single);
                if (loggingEnabled) {
                    log(OWNER_DISPATCHER, VERB_PAUSED, single.request.logId(),
                        "because tag '" + tag + "' was paused");
                }
            }

            if (hasMultiple) {
                for (int i = joined.size() - 1; i >= 0; i--) {
                    Action action = joined.get(i);
                    if (!action.getTag().equals(tag)) {
                        continue;
                    }

                    hunter.detach(action);
                    pausedActions.put(action.getTarget(), action);
                    if (loggingEnabled) {
                        log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
                            "because tag '" + tag + "' was paused");
                    }
                }
            }

            // Check if the hunter can be cancelled in case all its requests
            // had the tag being paused here.
            if (hunter.isCancelled()) {
                it.remove();
                if (loggingEnabled) {
                    log(OWNER_DISPATCHER, VERB_CANCELED, getLogIdsForHunter(hunter), "all actions paused");
                }
            }
        }
    }

    void performResumeTag(Object tag) {
        // Trying to resume a tag that is not paused.
        if (!pausedTags.remove(tag)) {
            return;
        }
        List<Action> batch = null;
        for (Iterator<Action> i = pausedActions.values().iterator(); i.hasNext();) {
            Action action = i.next();
            if (action.getTag().equals(tag)) {
                if (batch == null) {
                    batch = new ArrayList<>();
                }
                batch.add(action);
                i.remove();
            }
        }
        if (batch != null) {
            mainThreadHandler.sendEvent(InnerEvent.get(REQUEST_BATCH_RESUME,batch));
        }
    }

    void performComplete(BitmapHunter hunter) {
        if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
            cache.set(hunter.getKey(), hunter.getResult());
        }
        hunterMap.remove(hunter.getKey());
        batch(hunter);
        if (hunter.getPicasso().loggingEnabled) {
            log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
        }
    }

    void performBatchComplete() {
        List<BitmapHunter> copy = new ArrayList<>(batch);
        batch.clear();
        mainThreadHandler.sendEvent(InnerEvent.get(HUNTER_BATCH_COMPLETE, copy));
        logBatch(copy);
    }

    void performError(BitmapHunter hunter, boolean willReplay) {
        if (hunter.getPicasso().loggingEnabled) {
            log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter),
                "for error" + (willReplay ? " (will replay)" : ""));
        }
        hunterMap.remove(hunter.getKey());
        batch(hunter);
    }

    private void flushFailedActions() {
        if (!failedActions.isEmpty()) {
            Iterator<Action> iterator = failedActions.values().iterator();
            while (iterator.hasNext()) {
                Action action = iterator.next();
                iterator.remove();
                if (action.getPicasso().loggingEnabled) {
                    log(OWNER_DISPATCHER, VERB_REPLAYING, action.getRequest().logId());
                }
                performSubmit(action, false);
            }
        }
    }

    private void markForReplay(BitmapHunter hunter) {
        Action action = hunter.getAction();
        if (action != null) {
            markForReplay(action);
        }
        List<Action> joined = hunter.getActions();
        if (joined != null) {
            //noinspection ForLoopReplaceableByForEach
            for (int i = 0, n = joined.size(); i < n; i++) {
                Action join = joined.get(i);
                markForReplay(join);
            }
        }
    }

    private void markForReplay(Action action) {
        Object target = action.getTarget();
        if (target != null) {
            action.willReplay = true;
            failedActions.put(target, action);
        }
    }

    private void batch(BitmapHunter hunter) {
        if (hunter.isCancelled()) {
            return;
        }
        if (hunter.result != null) {
             hunter.result.createFromAlpha();  // TODO:  Need to check
        }
        batch.add(hunter);
        if (!handler.hasInnerEvent(HUNTER_DELAY_NEXT_BATCH)) {
            handler.sendEvent(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
        }
    }

    private void logBatch(List<BitmapHunter> copy) {
        if (copy == null || copy.isEmpty()) return;
        BitmapHunter hunter = copy.get(0);
        Picasso picasso = hunter.getPicasso();
        if (picasso.loggingEnabled) {
            StringBuilder builder = new StringBuilder();
            for (BitmapHunter bitmapHunter : copy) {
                if (builder.length() > 0) builder.append(", ");
                builder.append(Utils.getLogIdsForHunter(bitmapHunter));
            }
            log(OWNER_DISPATCHER, VERB_DELIVERED, builder.toString());
        }
    }

    private static class DispatcherHandler extends EventHandler {
        private final Dispatcher dispatcher;
        public DispatcherHandler(EventRunner runner, Dispatcher dispatcher) throws IllegalArgumentException {
            super(runner);
            this.dispatcher = dispatcher;
        }

        @Override
        protected void processEvent(InnerEvent event) {
            super.processEvent(event);
            switch (event.eventId) {
                case REQUEST_SUBMIT: {
                    Action action = (Action) event.object;
                    dispatcher.performSubmit(action);
                    break;
                }
                case REQUEST_CANCEL: {
                    Action action = (Action) event.object;
                    dispatcher.performCancel(action);
                    break;
                }
                case TAG_PAUSE: {
                    Object tag = event.object;
                    dispatcher.performPauseTag(tag);
                    break;
                }
                case TAG_RESUME: {
                    Object tag = event.object;
                    dispatcher.performResumeTag(tag);
                    break;
                }
                case HUNTER_COMPLETE: {
                    BitmapHunter hunter = (BitmapHunter) event.object;
                    dispatcher.performComplete(hunter);
                    break;
                }
                case HUNTER_RETRY: {
                    BitmapHunter hunter = (BitmapHunter) event.object;
                    dispatcher.performRetry(hunter);
                    break;
                }
                case HUNTER_DECODE_FAILED: {
                    BitmapHunter hunter = (BitmapHunter) event.object;
                    dispatcher.performError(hunter, false);
                    break;
                }
                case HUNTER_DELAY_NEXT_BATCH: {
                    dispatcher.performBatchComplete();
                    break;
                }
                case NETWORK_STATE_CHANGE: {
                    break;
                }
                case AIRPLANE_MODE_CHANGE: {
                    dispatcher.performAirplaneModeChange(event.eventId == AIRPLANE_MODE_CHANGE);
                    break;
                }
                default:
                    Picasso.HANDLER.postTask(new Runnable() {
                        @Override public void run() {
                            throw new AssertionError("Unknown handler message received: " + event.eventId);
                        }
                    });
            }
        }
    }
}

